package org.alexsotob.roo.addon.flyway;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedSet;
import java.util.logging.Logger;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.springframework.roo.file.monitor.event.FileDetails;
import org.springframework.roo.process.manager.FileManager;
import org.springframework.roo.project.Dependency;
import org.springframework.roo.project.Path;
import org.springframework.roo.project.ProjectOperations;
import org.springframework.roo.support.logging.HandlerUtils;
import org.springframework.roo.support.util.Assert;
import org.springframework.roo.support.util.FileCopyUtils;
import org.springframework.roo.support.util.StringUtils;
import org.springframework.roo.support.util.TemplateUtils;
import org.springframework.roo.support.util.XmlUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

@Component
@Service
public class FlywayOperationsImpl implements FlywayOperations {

	private static final String COMMENT_SEPARATOR = "__";

	private static final String FLYWAY_BEAN_ID = "flyway";

	private static final String DATA_SOURCE_DEFAULT_BEAN_ID = "dataSource";

	private static final Logger LOGGER = HandlerUtils
			.getLogger(FlywayOperationsImpl.class);

	private static final char SEPARATOR = File.separatorChar;

	static final String PERSISTENCE_XML = "persistence.xml";

	static final String APPLICATION_CONTEXT_XML = "applicationContext.xml";

	static final String[] ENTITY_MANAGER_FACTORY_BEAN_CLASSES = new String[] {
			"org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean",
			"org.springframework.orm.jpa.LocalEntityManagerFactoryBean" };

	@Reference
	FileManager fileManager;

	@Reference
	ProjectOperations projectOperations;

	/** {@inheritDoc} */
	@Override
	public boolean isFlywayInstallationPossible() {
		return projectOperations.isProjectAvailable()
				&& fileManager.exists(projectOperations
						.getProjectMetadata()
						.getPathResolver()
						.getIdentifier(Path.SRC_MAIN_RESOURCES,
								"META-INF" + SEPARATOR + PERSISTENCE_XML));
	}

	/** {@inheritDoc} */
	@Override
	public boolean isNewPlainSqlFileCreationPossible() {
		return isDependencyRegistered();
	}

	/** {@inheritDoc} */
	@Override
	public boolean isNewJavaMigrationClassCreationPossible() {
		return isDependencyRegistered();
	}

	@Override
	public boolean isFlywayUninstallationPossible() {
		return isDependencyRegistered();
	}

	private boolean isDependencyRegistered() {

		Element configuration = XmlUtils.getConfiguration(getClass());
		List<Element> flywayDependencies = XmlUtils.findElements(
				"/configuration/flyway/dependencies/dependency", configuration);

		for (Element dependency : flywayDependencies) {
			// With Spring Roo 1.2.0 project operations have an
			// isInstalledFeature,
			// but for backwards compatibility dependency attribute is used.
			boolean dependencyRegistered = this.projectOperations
					.getProjectMetadata().isDependencyRegistered(
							new Dependency(dependency));
			if (!dependencyRegistered) {
				return false;
			}
		}

		return true;

	}

	/** {@inheritDoc} */
	@Override
	public void newJavaMigrationClass(String version, String description) {

		String javaMigrationFilename = buildJavaMigrationFilename(version,
				description);
		String destinationFile = this.projectOperations.getPathResolver()
				.getIdentifier(
						Path.SRC_MAIN_JAVA,
						"db" + SEPARATOR + "migration" + SEPARATOR
								+ javaMigrationFilename);

		if (!existMigrationFileIgnoringCommentsField(javaMigrationFilename)) {

			InputStream javaMigrationTemplateInputStream = TemplateUtils
					.getTemplate(getClass(), "JavaMigration.java-template");
			Assert.notNull(javaMigrationTemplateInputStream,
					"Java Migration template is not found on addon.");

			try {
				createJavaMigrationClass(javaMigrationFilename,
						destinationFile, javaMigrationTemplateInputStream);
			} catch (IOException e) {
				throw new IllegalStateException(
						"Unable to create Java Migration file.", e);
			}
		} else {
			LOGGER.warning("Migration file with version " + version
					+ " already exists");
		}
	}

	private void createJavaMigrationClass(String javaMigrationFilename,
			String destinationFile, InputStream javaMigrationTemplateInputStream)
			throws IOException {
		String content = FileCopyUtils.copyToString(new InputStreamReader(
				javaMigrationTemplateInputStream));
		content = replaceTemplateExpressions(javaMigrationFilename, content);

		saveOrUpdateFile(destinationFile, content);
	}

	private String replaceTemplateExpressions(String javaMigrationFilename,
			String content) {
		content = content.replace("__TOP_LEVEL_PACKAGE__", "db.migration");
		content = content.replace(
				"__APP_NAME__",
				javaMigrationFilename.substring(0,
						javaMigrationFilename.lastIndexOf('.')));
		return content;
	}

	private String buildJavaMigrationFilename(String version, String comment) {

		StringBuilder fileNameBuilder = new StringBuilder("V");
		fileNameBuilder.append(version.replace('.', '_'));

		if (comment != null) {
			fileNameBuilder.append(COMMENT_SEPARATOR);
			fileNameBuilder.append(comment);
		}

		fileNameBuilder.append(".java");
		return fileNameBuilder.toString();
	}

	/** {@inheritDoc} */
	@Override
	public void newPlainSqlFile(String version, String description) {

		Assert.notNull(version, "Version is required.");

		String sqlFilename = buildPlainSqlFilename(version, description);

		String versionedSqlScriptPath = this.projectOperations
				.getPathResolver().getIdentifier(
						Path.SRC_MAIN_RESOURCES,
						"db" + SEPARATOR + "migration" + SEPARATOR
								+ sqlFilename);

		if (!existMigrationFileIgnoringCommentsField(sqlFilename)) {
			createVersionedSqlScriptFile(versionedSqlScriptPath);
		} else {
			LOGGER.warning("Migration file with version " + version
					+ " already exists");
		}

	}

	private void createVersionedSqlScriptFile(String versionedSqlScriptPath) {
		InputStream sqlTemplateInputStream = TemplateUtils.getTemplate(
				getClass(), "flyway-template.sql");
		Assert.notNull(sqlTemplateInputStream,
				"Flyway template is not found on addon.");

		try {
			saveOrUpdateFile(versionedSqlScriptPath,
					FileCopyUtils.copyToString(new InputStreamReader(
							sqlTemplateInputStream)));
		} catch (IOException e) {
			throw new IllegalStateException(e);
		}
	}

	private String buildPlainSqlFilename(String version, String comment) {

		StringBuilder fileNameBuilder = new StringBuilder("V");
		fileNameBuilder.append(version.replace('.', '_'));

		if (comment != null) {
			fileNameBuilder.append(COMMENT_SEPARATOR);
			fileNameBuilder.append(comment);
		}

		fileNameBuilder.append(".sql");
		return fileNameBuilder.toString();
	}

	@Override
	public void removeFlyway() {

		removeFlywayDependencies();
		removeFlywayInApplicationContext();

	}

	private void removeFlywayInApplicationContext() {

		final Document appCtx = readXmlFile(Path.SPRING_CONFIG_ROOT,
				APPLICATION_CONTEXT_XML);

		unregisterPreviousFlywayBean(appCtx);
		removeDependosOnAttributeFromDataSource(appCtx);

		saveOrUpdateFile(
				projectOperations.getPathResolver().getIdentifier(
						Path.SPRING_CONFIG_ROOT, APPLICATION_CONTEXT_XML),
				XmlUtils.nodeToString(appCtx));
	}

	private void removeDependosOnAttributeFromDataSource(final Document appCtx) {

		Element entityManagerFactoryBean = getRegisteredEntityManagerFactoryBean(appCtx
				.getDocumentElement());

		if (entityManagerFactoryBean != null) {
			entityManagerFactoryBean.removeAttribute("depends-on");
		}
	}

	private void removeFlywayDependencies() {

		Element configuration = XmlUtils.getConfiguration(getClass());

		List<Dependency> dependencies = new ArrayList<Dependency>();
		List<Element> flywayDependencies = XmlUtils.findElements(
				"/configuration/flyway/dependencies/dependency", configuration);
		for (Element dependencyElement : flywayDependencies) {
			dependencies.add(new Dependency(dependencyElement));
		}

		projectOperations.removeDependencies(dependencies);
	}

	/** {@inheritDoc} */
	@Override
	public void configureFlyway(String dataSourceId) {

		addFlywayDependencies();
		updateFlywayBeanInApplicationContext(dataSourceId);
		updatePersistenceUnitAvoidingDatabaseAutoCreation();

	}

	private void addFlywayDependencies() {

		Element configuration = XmlUtils.getConfiguration(getClass());

		List<Dependency> dependencies = new ArrayList<Dependency>();
		List<Element> flywayDependencies = XmlUtils.findElements(
				"/configuration/flyway/dependencies/dependency", configuration);
		for (Element dependencyElement : flywayDependencies) {
			dependencies.add(new Dependency(dependencyElement));
		}
		projectOperations.addDependencies(dependencies);

	}

	private void updateFlywayBeanInApplicationContext(String dataSourceId) {

		final Document appCtx = readXmlFile(Path.SPRING_CONFIG_ROOT,
				APPLICATION_CONTEXT_XML);

		unregisterPreviousFlywayBean(appCtx);
		registerFlywayBean(dataSourceId, appCtx);
		updateDependsOnPropertyOnEntityManagerFactoryBean(appCtx);
		saveOrUpdateFile(
				projectOperations.getPathResolver().getIdentifier(
						Path.SPRING_CONFIG_ROOT, APPLICATION_CONTEXT_XML),
				XmlUtils.nodeToString(appCtx));
	}

	private void unregisterPreviousFlywayBean(final Document appCtx) {
		Element flyway = getBeanDefinitionById(FLYWAY_BEAN_ID,
				appCtx.getDocumentElement());

		if (flyway != null) {
			appCtx.getDocumentElement().removeChild(flyway);
		}
	}

	private void registerFlywayBean(String dataSourceId, final Document appCtx) {
		Element flyway = appCtx.createElement("bean");
		flyway.setAttribute("class", "com.googlecode.flyway.core.Flyway");
		flyway.setAttribute("id", FLYWAY_BEAN_ID);
		flyway.setAttribute("init-method", "migrate");

		String validDatasourceId = getValidDatasourceId(dataSourceId,
				appCtx.getDocumentElement());
		flyway.appendChild(createPropertyElementWithRef("dataSource",
				validDatasourceId, appCtx));
		appCtx.getDocumentElement().appendChild(flyway);
	}

	private String getValidDatasourceId(String dataSourceId, Element root) {

		if (StringUtils.hasText(dataSourceId)) {

			validateDatasourceBeanId(dataSourceId, root);
			return dataSourceId;
		} else {
			validateDatasourceBeanId(DATA_SOURCE_DEFAULT_BEAN_ID, root);
			return DATA_SOURCE_DEFAULT_BEAN_ID;
		}

	}

	private void validateDatasourceBeanId(String dataSourceId, Element root) {
		Element dataSourceBean = getBeanDefinitionById(dataSourceId, root);

		if (dataSourceBean == null) {
			LOGGER.warning("Data source with bean id "
					+ dataSourceId
					+ " is not registered, but will be used as property in flyway. To change rerun same command with correct data source id.");
		}
	}

	private void updateDependsOnPropertyOnEntityManagerFactoryBean(
			Document appCtx) {

		Element entityManagerFactoryBean = getRegisteredEntityManagerFactoryBean(appCtx
				.getDocumentElement());

		if (entityManagerFactoryBean != null) {
			entityManagerFactoryBean.setAttribute("depends-on", FLYWAY_BEAN_ID);
		} else {
			LOGGER.warning("No Entity Manager Factory Bean is registered.");
		}

	}

	private Element getRegisteredEntityManagerFactoryBean(Element root) {

		Element entityManagerFactoryBean = null;

		for (String entityManagerFactoryBeanClass : ENTITY_MANAGER_FACTORY_BEAN_CLASSES) {
			entityManagerFactoryBean = getBeanDefinitionByClass(
					entityManagerFactoryBeanClass, root);

			if (entityManagerFactoryBean != null) {
				return entityManagerFactoryBean;
			}

		}

		return entityManagerFactoryBean;

	}

	private Element getBeanDefinitionByClass(String beanClass, Element root) {
		Element beanElement = XmlUtils.findFirstElement(
				"/beans/bean[@class = '" + beanClass + "']", root);

		return beanElement;
	}

	private Element getBeanDefinitionById(String beanId, Element root) {

		Element beanElement = XmlUtils.findFirstElement("/beans/bean[@id = '"
				+ beanId + "']", root);

		return beanElement;
	}

	private Element createPropertyElementWithRef(final String name,
			final String value, final Document document) {
		final Element property = document.createElement("property");
		property.setAttribute("name", name);
		property.setAttribute("ref", value);
		return property;
	}

	private void updatePersistenceUnitAvoidingDatabaseAutoCreation() {

		Document persistence = readXmlFile(Path.SRC_MAIN_RESOURCES, "META-INF"
				+ SEPARATOR + PERSISTENCE_XML);

		avoidDatabaseAutoCreation(persistence);

		this.saveOrUpdateFile(
				projectOperations.getPathResolver().getIdentifier(
						Path.SRC_MAIN_RESOURCES,
						"META-INF" + SEPARATOR + PERSISTENCE_XML),
				XmlUtils.nodeToString(persistence));

	}

	private void avoidDatabaseAutoCreation(Document persistence) {
		PersistenceFileModifier persistenceFileModifier = getPersistenceFileModifier(persistence);
		Assert.notNull(persistenceFileModifier,
				"Persistence provider not recognized");
		persistenceFileModifier.avoidDatabaseAutoCreation(persistence);
	}

	private void saveOrUpdateFile(String path, String content) {
		this.fileManager.createOrUpdateTextFileIfRequired(path, content, false);
	}

	private PersistenceFileModifier getPersistenceFileModifier(
			Document persistence) {

		String persistenceProvider = getPersistenceProvider(persistence);

		return PersistenceFileModifierFactory.create(persistenceProvider);
	}

	private String getPersistenceProvider(Document persistence) {

		Element persistenceProvider = XmlUtils.findFirstElement(
				"/persistence/persistence-unit/provider",
				persistence.getDocumentElement());
		Assert.notNull(persistenceProvider,
				"No persistence provider is specified.");

		return persistenceProvider.getTextContent();
	}

	private Document readXmlFile(Path path, String relativePath) {

		final String xmlPath = projectOperations.getPathResolver()
				.getIdentifier(path, relativePath);

		return XmlUtils.readXml(fileManager.getInputStream(xmlPath));
	}

	private boolean existMigrationFileIgnoringCommentsField(String filePath) {

		String rootPath = projectOperations.getPathResolver()
				.getRoot(Path.ROOT);

		String fileAntPattern = removeCommentsAndFileExtension(filePath);
		fileAntPattern = rootPath + "/**/" + fileAntPattern + "*.*";

		SortedSet<FileDetails> matchingFiles = this.fileManager
				.findMatchingAntPath(fileAntPattern.replace('/', SEPARATOR));

		return matchingFiles.size() > 0;
	}

	private String removeCommentsAndFileExtension(String filePath) {

		int commentSeparator = filePath.lastIndexOf(COMMENT_SEPARATOR);

		int lastValidCharacterIndex = commentSeparator == -1 ? filePath
				.lastIndexOf('.') : commentSeparator;

		return filePath.substring(0, lastValidCharacterIndex);
	}

}